
%% Copyright (c) 2006 Christophe Romain <cromain@process-one.net>
%%
%% Permission is hereby granted, free of charge, to any person
%% obtaining a copy of this software and associated documentation
%% files (the "Software"), to deal in the Software without restriction,
%% including without limitation the rights to use, copy, modify, merge,
%% publish, distribute, sublicense, and/or sell copies of the Software,
%% and to permit persons to whom the Software is furnished to do
%% so, subject to the following conditions:
%%
%% The above copyright notice and this permission notice shall be included
%% in all copies or substantial portions of the Software.
%%
%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
%% MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
%% IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
%% CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
%% TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
%% SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

%% @doc The ErlyDB Mnesia driver.

-module(erlydb_mnesia).
-author('christophe.romain@process-one.net').

-export([
	connect/5,
	import/0,
	export/0,
	select/3,
	insert/2,
	update/3,
	delete/2,
	count/2,
	max/3,
	nextid/1,
	size/1,
	sql/1 ]).


%% @doc Initialize a Mnesia session.
connect(_Hostname, _Username, _Password, _Database, _Options) ->
	connect().
connect() ->
	Nodes = [node() | nodes()],
	mnesia:create_schema(Nodes),
	lists:foreach(
		fun(Node) -> 
			rpc:call(Node, mnesia, start, []) 
		end, Nodes),
	ok.

%% @doc XML data import function
import() ->
	%mnesia:restore(File, []).
	not_implemented.

%% @doc XML data export export function
export() ->
	%mnesia:dump_to_textfile(File++".txt"),
	%mnesia:backup(File).
	not_implemented.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%% Mnesia helpers

%% @doc Apply mnesia action
%mnesia(Action, Param) when is_list(Param) ->
%	apply(mnesia, Action, Param);
%mnesia(Action, Param) ->
%	mnesia:Action(Param).
mnesia(Action, Params) ->
    Trans = fun() -> apply(mnesia, Action, Params) end,
    case mnesia:transaction(Trans) of
	{atomic, Status} -> Status;
	{aborted, _Reason} -> []
    end.

%% @doc Build guard spec (used by select)
where_to_guards(Table, Where) ->
	lists:map(fun
		({Field, '=', Value}) ->
			{'==', field_to_match(Table, Field), Value};
		({Field, Op, Value}) when Op=='=='; Op=='<'; Op=='>' ->
			{Op, field_to_match(Table, Field), Value};
		(_) -> []
		end, Where).

%% @doc Get undefined record
undefined_record(Table) ->
	Len = mnesia:table_info(Table, arity) - 1,
	list_to_tuple([Table | lists:duplicate(Len, undefined)]).

%% @doc Get record with filled values
filled_record(Table, Values) ->
	filled_record(Table, Values, undefined_record(Table)).
filled_record(Table, Values, OldRecord) ->
	lists:foldl(
		fun({Field, Value}, Rec) -> 
			setelement(field_to_idx(Table, Field)+1, Rec, Value)
		end, OldRecord, Values).

%% @doc Get total match record
match_all_record(Table) ->
	Len = mnesia:table_info(Table, arity) - 1,
	list_to_tuple([Table | lists:duplicate(Len, '_')]).

%% @doc Get matching record
%% match_record(Table, Where) ->
%% 	lists:foldl(
%% 		fun
%% 		({Field, Op, Value}, Record) when Op=='=='; Op=='=' -> 
%% 			setelement(field_to_idx(Table, Field)+1, Record, Value);
%% 		(_, Record) ->
%% 			Record
%%		end, match_all_record(Table), Where).

%% @doc Get select record
select_record(Table, Fields, Where) ->
	MatchRec = lists:foldl(
		fun(Field, Record) ->
			case field_to_idx(Table, Field) of
			0 -> 
				Record;
			N ->
				% not really efficient, to be improved
				setelement(N+1, Record, field_to_match(N))
			end
		end, match_all_record(Table), Fields),
	lists:foldl(
		fun
		({Field, _, _}, Record) ->
			N = field_to_idx(Table, Field),
			setelement(N+1, Record, field_to_match(N));
		(_, Record) ->
			Record
		end, MatchRec, Where).

%% @doc Find field position in a table
field_to_idx(Table, Field) -> 
	field_to_idx(Field, 1, mnesia:table_info(Table, attributes)).
field_to_idx(Field, Pos, [Field|_]) -> Pos;
field_to_idx(Field, Pos, [_|Tail]) -> field_to_idx(Field, Pos+1, Tail);
field_to_idx(_, _, []) -> 0.

%% @doc Get field matching name
field_to_match(Number) when Number<10 ->
	%% efficient but limited, to be improved
	lists:nth(Number+1,['_','$1','$2','$3','$4','$5','$6','$7','$8','$9']);
field_to_match(_) -> '_'.
field_to_match(Table, Field) ->
	field_to_match(field_to_idx(Table, Field)).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%% SQL helpers

%% @doc Convert word in lower case
tolower(Word) -> 
	lists:map(fun
	(Char) when $A =< Char, Char =< $Z -> Char + 32;
	(Char) -> Char
	end, Word).

%% @doc Convert word in upper case
%toupper(Word) ->
%	lists:map(fun
%	(Char) when $a =< Char, Char =< $z -> Char - 32;
%	(Char) -> Char
%	end, Word).

%% @doc Get record from SQL tokens
sql_record(Table, Values) ->
	{_, Record} = lists:foldl(fun(Value, {Pos, Rec}) ->
		{Int, _} = string:to_integer(Value),
		Real = case Int of
			error -> 
				Stripped = string:strip(Value, both, $'), % for vim syntax color '
				case length(Stripped) of
					0 -> undefined;
					_ -> Stripped
				end;
			_ -> Int
		end,
		{Pos+1, setelement(Pos, Rec, Real)}
		end, {2, undefined_record(list_to_atom(Table))}, Values),
	Record.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% ErlyDB Database Actions

select(Table, Fields, Where) ->
	Match = select_record(Table, Fields, Where),
	Guards = where_to_guards(Table, Where),
	Results = case Fields of
		[] -> ['$_'];
		_ -> [lists:map(fun(Field) -> field_to_match(Table, Field) end, Fields)]
	end,
	mnesia(select, [Table, [{Match, Guards, Results}]]).

insert(Table, Values) ->
	mnesia(write, [filled_record(Table, Values)]).

update(Table, Values, Where) ->
	Select = select(Table, [], Where),
	lists:foreach(
		fun(Record) ->
			mnesia(write, [filled_record(Table, Values, Record)])
		end, Select),
	length(Select).

delete(Table, Where) ->
	%simple match, best speed
	%Match = match_record(Table, Where), %% only '=' clause taken 
	%mnesia(delete_object, [Match]).
	Select = select(Table, [], Where),
	lists:foreach(
		fun(Record) ->
			mnesia(delete_object, [Record])
		end, Select),
	length(Select).

count(Table, Where) -> 
	length(select(Table, [], Where)).

max(Table, Field, Where) -> 
	lists:max(lists:flatten(select(Table, [Field], Where))).

nextid(Table) ->
	%size(Table) + 1.
	max(Table, id, []) + 1.

size(Table) ->
	mnesia:table_info(Table, size).

sql(SqlQuery) ->
	Tokens = string:tokens(tolower(SqlQuery), " (), ;"),
	case lists:nth(1, Tokens) of
	"insert" ->
		T = lists:nth(3, Tokens),
		S = string:substr(SqlQuery, string:chr(SqlQuery, $()+1),
		V = string:tokens(S, "(), ;"),
		mnesia(write, [sql_record(T, V)]);
	"update" ->
		T = lists:nth(2, Tokens),
		%% format to improve
		S = string:substr(SqlQuery, string:chr(SqlQuery, $()+1),
		V = string:tokens(S, " =;"),
		mnesia(write, [sql_record(T, V)]);
	"select" ->
		_ = lists:nth(3, Tokens),
		%% format to do
		% where
		% OK: where field = "good"
		% KO: where field="bad"
		%case lists:nth(1, Tokens) of
		%"where" ->
		%	[F, O, V] = lists:sublist(Tokens, 2, 3),
		%	Op = case O of
		%		"=" -> "==";
		%		_ -> Op
		%	end,
		%	"[{"++F++", '"++Op++"', "++V++"}]";
		%_ ->
		%	"[]"
		%end,
		badcommand;
	_ ->
		badcommand
	end.
